Skip to content

fix(iOS): keep prebuilt Headers/ in place on a Debug/Release swap - #57814

Closed
chrfalch wants to merge 1 commit into
mainfrom
chrfalch/fix-rncore-swap-modulemap
Closed

fix(iOS): keep prebuilt Headers/ in place on a Debug/Release swap#57814
chrfalch wants to merge 1 commit into
mainfrom
chrfalch/fix-rncore-swap-modulemap

Conversation

@chrfalch

@chrfalch chrfalch commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #57803. An iOS Release build can fail in PrecompileModule React with seven include of non-modular header inside framework module errors — but only when the build follows a Debug/Release configuration switch, and only when the project contains a pod target that does not depend on React.

replace-rncore-version.js deleted and recreated Pods/React-Core-prebuilt/Headers/ on a swap. That directory holds module.modulemap, which rncore.rb activates on every target through -fmodule-map-file.

A pod that depends on React has a target-dependency edge that orders its module scan behind React-Core-prebuilt's script phase. A pod with no React dependency has no such edge, so its scan is free to run before the swap. The React module is then precompiled without the module map, and <yoga/...>, <react/...> and <RCTDeprecation/...> resolve textually and trip -Wnon-modular-include-in-framework-module.

Those headers never needed replacing. The prebuild compose job emits one set of ReactNativeHeaders for both configurations, so they are identical in the Debug and Release tarballs — only the compiled framework differs. This replaces React.xcframework and nothing else, so the module map stays put for the whole build and the ordering stops mattering.

Changelog:

[IOS] [FIXED] - Keep the prebuilt Headers/ in place on a Debug/Release configuration switch so the React explicit module still resolves its module map

Test Plan

The premise, on the published 0.87.0-rc.3 artifacts (ios-arm64_x86_64-simulator):

compared between the Debug and Release tarballs result
ReactNativeHeaders…/Headers/module.modulemap identical
React.framework/Modules/module.modulemap identical
ReactNativeHeaders…/Headers tree (diff -rq) 0 differences
React.framework/Headers tree (diff -rq) 0 differences

The reproducer from #57803, on Xcode 26.3 with CocoaPods 1.15.2:

build result
0.87.0-rc.3 FAIL — exit 65, 7 errors
0.87.0-rc.3 + this PR PASS** BUILD SUCCEEDED **, 0 errors

The swap still does its job in the passing build — it logs Replacing React-Core-prebuilt/React.xcframework, and the installed binary is the Release one:

installed:   55225ccbc283c57c614ff4caf263cb63bad3828240e62cee8893e7001774bd6c
rc3 release: 55225ccbc283c57c614ff4caf263cb63bad3828240e62cee8893e7001774bd6c
rc3 debug:   516215801a6f8a86640aae13c2f2de1bbdb95189edf124e208f528b1497c7e4c

A Release→Debug swap was verified the same way. Across a swap, Headers/module.modulemap keeps its inode while React.xcframework gets a new one.

Isolating the precondition

On stock 0.87.0-rc.3, changing only the reproducer's IndependentPod, and reading back which targets' ScanDependencies ran before the swap:

IndependentPod.podspec scans before the swap result
no dependencies (as reported) IndependentPod FAIL 2/2
+ spec.dependency 'React-Core' none PASS 2/2
pod removed from the Podfile none PASS 2/2

Adding the dependency was the only change — same Podfile declaration, same local :path pod, same source file importing only Foundation.

Note this is an ordering race, not a structural guarantee: a project with no React-independent pod is very likely safe but not provably so, since a different job count or machine load could reorder scans. This PR removes the race rather than the ordering, so the precondition stops mattering either way.

Adds a unit test for the script (3 cases: correct framework installed, module map untouched, fail on a tarball with no React.xcframework). The script needed a require.main === module guard and one export to be importable.

Note for the release crew

This is a candidate for 0.87.0-rc.4. #57803 reproduces on rc.2 and rc.3 and is not fixed by anything currently on 0.87-stable; I verified #57742 in particular does not address it.

Exposure: a stock template app has no React-independent pod and passed here. Apps that pull a native SDK pod directly (Firebase, Sentry, analytics or networking libraries) or vendor a local utility pod are the exposed shape, including via transitive pods of an RN wrapper.

Not fixed here: React.xcframework is still replaced mid-build, and Xcode's xcframework-extraction task is not ordered against this script phase either. No failure was observed from that, and closing it properly means installing both configurations side by side instead of mutating pod content during a build — too large for a release cut.

🤖 Generated with Claude Code

An iOS Release build can fail in `PrecompileModule React` with seven
`include of non-modular header inside framework module` errors, but only
when the build follows a Debug/Release configuration switch (#57803).

`replace-rncore-version.js` deleted and recreated
`Pods/React-Core-prebuilt/Headers/` on a swap. That directory holds
`module.modulemap`, which `rncore.rb` activates on every target through
`-fmodule-map-file`. Nothing orders an unrelated target's dependency scan
against this script phase, so a scan can run while the module map is
missing; the React module is then precompiled without it and `<yoga/...>`,
`<react/...>` and `<RCTDeprecation/...>` resolve non-modularly.

Those headers never needed replacing. The prebuild compose job emits one
set of ReactNativeHeaders for both configurations, so they are identical
in the Debug and Release tarballs — only the compiled framework differs.
Replace `React.xcframework` and nothing else.

## Changelog:

[IOS] [FIXED] - Keep the prebuilt `Headers/` in place on a Debug/Release configuration switch so the React explicit module still resolves its module map

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 4, 2026
@facebook-github-tools facebook-github-tools Bot added p: Expo Partner: Expo Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Aug 4, 2026
@meta-codesync

meta-codesync Bot commented Aug 4, 2026

Copy link
Copy Markdown

@fabriziocucci has imported this pull request. If you are a Meta employee, you can view this in D114735639.

@meta-codesync

meta-codesync Bot commented Aug 4, 2026

Copy link
Copy Markdown

@fabriziocucci merged this pull request in df5e6f6.

react-native-bot pushed a commit that referenced this pull request Aug 4, 2026
…7814)

Summary:
Fixes #57803. An iOS Release build can fail in `PrecompileModule React` with seven `include of non-modular header inside framework module` errors — but only when the build follows a Debug/Release configuration switch.

`replace-rncore-version.js` deleted and recreated `Pods/React-Core-prebuilt/Headers/` on a swap. That directory holds `module.modulemap`, which `rncore.rb` activates on every target through `-fmodule-map-file`. Nothing orders an unrelated target's dependency scan against this script phase, so a scan can run while the module map is missing. The React module is then precompiled without it, and `<yoga/...>`, `<react/...>` and `<RCTDeprecation/...>` resolve non-modularly.

Those headers never needed replacing. The prebuild compose job emits one set of ReactNativeHeaders for both configurations, so they are identical in the Debug and Release tarballs — only the compiled framework differs. This replaces `React.xcframework` and nothing else.

## Changelog:

[IOS] [FIXED] - Keep the prebuilt `Headers/` in place on a Debug/Release configuration switch so the React explicit module still resolves its module map

Pull Request resolved: #57814

Test Plan:
The premise, on the published 0.87.0-rc.3 artifacts (`ios-arm64_x86_64-simulator`):

| compared between the Debug and Release tarballs | result |
| --- | --- |
| `ReactNativeHeaders…/Headers/module.modulemap` | identical |
| `React.framework/Modules/module.modulemap` | identical |
| `ReactNativeHeaders…/Headers` tree (`diff -rq`) | 0 differences |
| `React.framework/Headers` tree (`diff -rq`) | 0 differences |

The reproducer from #57803, on Xcode 26.3 with CocoaPods 1.15.2:

| build | result |
| --- | --- |
| 0.87.0-rc.3 | **FAIL** — exit 65, 7 errors |
| 0.87.0-rc.3 + this PR | **PASS** — `** BUILD SUCCEEDED **`, 0 errors |

The swap still does its job in the passing build — it logs `Replacing React-Core-prebuilt/React.xcframework`, and the installed binary is the Release one:

```
installed:   55225ccbc283c57c614ff4caf263cb63bad3828240e62cee8893e7001774bd6c
rc3 release: 55225ccbc283c57c614ff4caf263cb63bad3828240e62cee8893e7001774bd6c
rc3 debug:   516215801a6f8a86640aae13c2f2de1bbdb95189edf124e208f528b1497c7e4c
```

A Release→Debug swap was verified the same way. Across a swap, `Headers/module.modulemap` keeps its inode while `React.xcframework` gets a new one.

## Unit tests

Adds a unit test for the script, 4 cases: correct framework installed, `Headers/module.modulemap` untouched, an Expo-generated `React-use-frameworks.modulemap` left in place, and a fail-closed case on a tarball with no `React.xcframework`. The script needed a `require.main === module` guard and one export to be importable.

```
js1 test xplat/js/react-native-github/packages/react-native/scripts/__tests__/replace-rncore-version-test.js
→ 4 passed, 4 total
```

The module-map case is a real regression test, not just a pin. Restoring the pre-fix delete-and-recreate makes it fail on the inode assertion while the other three keep passing:

```
✕ leaves Headers/module.modulemap untouched
  Expected: 735095485
  Received: 735095515
```

That only works because the fixture tarball also ships `ReactNativeHeaders.xcframework`. Without it the pre-fix code throws its fail-closed error before reaching the assertion, so the test would go red for the wrong reason and would not actually be guarding #57803.

The Expo case covers behaviour this diff removes the explicit protection for. The old save-and-restore of `React-use-frameworks.modulemap` (ada3922) is unnecessary now that only `React.xcframework` is replaced, but nothing else pins it.

## Argument parsing

`yargs` parsing moved inside the `require.main === module` guard, so importing the module no longer parses `process.argv`. Verified in both directions.

The command line still performs the swap end to end:

```
$ node replace-rncore-version.js -c Release -r <version> -p <podsRoot>
Replacing React-Core-prebuilt/React.xcframework
Updating React-Core-prebuilt/.last_build_configuration with Release
Done replacing React Native prebuilt

installed binary: binary-Release
module.modulemap inode before=735141703 after=735141703
last_build marker: Release
```

Importing with hostile argv (`-c` collides with jest's `--config`) has no side effects:

```
$ node -e "process.argv = ['node','jest','-c','jest.config.js','--version']; require('./replace-rncore-version.js')"
imported OK, exports: replaceRNCoreConfiguration
```

`arc lint` is clean on both files.

Reviewed By: zeyap

Differential Revision: D114735639

Pulled By: fabriziocucci

fbshipit-source-id: 35ead7dae9ce7ad7160005a15ecb3975817ae728
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. p: Expo Partner: Expo Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS prebuilt core: React explicit-module build fails after a Debug-to-Release swap

1 participant